/*******************************************************************************
 * Copyright (c) 2008 Scott Stanchfield.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Scott Stanchfield - initial API and implementation
 *******************************************************************************/
package com.javadude.annotation.processors;

import java.io.PrintWriter;
import java.util.Iterator;
import java.util.List;

public class Generator {
	private static final String PADDING = "                                                                                                                        ";
	private Symbols symbols_ = new Symbols();
	private final PrintWriter writer_;
	private final Data data_;

	public Generator(PrintWriter writer, Data data) {
		writer_ = writer;
		data_ = data;
		symbols_ = new Symbols(data.createPropertyMap());
	}
	private String fill(String template) {
		while (true) {
			int start = template.indexOf("${");
			if (start == -1) {
	            break;
            }
			int end = template.indexOf('}', start + 2);
			if (end == -1) {
				throw new RuntimeException("Template has mismatched ${...}");
			}
			String field = template.substring(start + 2, end);
			Object value = symbols_.get(field,-1);
			if (value == null) {
	            throw new RuntimeException("${" + field + "} defined in template not found");
            }
			if (end == template.length()) {
				template = template.substring(0, start) + value;
            } else {
            	template = template.substring(0, start) + value + template.substring(end + 1);
            }
		}
		return template;
	}

	private void gl(boolean include, String line) {
		if (include) {
	        gl(line);
        }
	}
	private void gp(boolean include, String line) {
		if (include) {
			gp(line);
		}
	}
	private void gl(String line) {
		gp(line);
       	writer_.println();
	}
	private void gp(String line) {
		// TODO: check that we have enough room in the PADDING string - increase if necessary
		if (data_.getSpacesForLeadingTabs() > 0) {
			int i = 0;
			while (i < line.length() && line.charAt(i) == '\t') {
				i++;
			}
			if (i > 0) {
				line = Generator.PADDING.substring(0, i * data_.getSpacesForLeadingTabs()) + line.substring(i);
			}
		}
		writer_.print(fill(line));
	}

	private abstract class ForEach<T extends Pushable> {
		protected boolean isLast() { return last_; }
		protected boolean isFirst() { return first_; }
		private boolean last_ = false;
		private boolean first_ = true;
		public ForEach(List<T> items) {
			for (Iterator<T> i = items.iterator(); i.hasNext();) {
				try {
					T item = i.next();
					last_ = !i.hasNext();
					symbols_.pushScope(item.createPropertyMap());
					go(item);
					first_ = false;
				} finally {
					symbols_.popScope();
				}
			}
		}
		protected abstract void go(T item);
	}


	public void generate() {
		gl(												"// CODE GENERATED BY JAVADUDE BEAN ANNOTATION PROCESSOR");
		gl(												"// -- DO NOT EDIT  -  THIS CODE WILL BE REGENERATED! --");
		gl(												"package ${packageName};");

		// class definition
		gl(												"@javax.annotation.Generated(");
		gl(												"	value = \"com.javadude.annotation.processors.BeanAnnotationProcessor\",");
		gl(												"	date = \"${date}\",");
		gl(												"	comments = \"CODE GENERATED BY JAVADUDE BEAN ANNOTATION PROCESSOR; DO NOT EDIT! THIS CODE WILL BE REGENERATED!\")");

		gl(												"@java.lang.SuppressWarnings(\"all\")");
		gp(												"${classAccess}abstract class ${className}Gen ");
		gp(data_.getSuperclass() != null,				"extends ${superclass} ");
		gp(data_.isCloneable(),							"implements Cloneable ");
		gl(												"{");

		new ForEach<DelegateSpec>(data_.getDelegates()) {
			@Override protected void go(DelegateSpec item) {
				gl(item.isNeedToDefine(),				"	private ${name} ${accessor};");
			}};

		gl(												"	public ${className}Gen(${superConstructorArgs}) {");
		gl(data_.getSuperConstructorSuperCall() != null,"		${superConstructorSuperCall};");

		new ForEach<DelegateSpec>(data_.getDelegates()) {
			@Override protected void go(DelegateSpec item) {
				gl(item.getInstantiateType() != null,	"		${accessor} = new ${instantiateType}();");
				// TODO better specification of instance creation - might depend on properties... lazy instantiate?
			}};

		gl(												"	}");

		gl(data_.isCloneable(),							"	public ${className} clone() {");
		gl(data_.isCloneable(),							"		try {");
		gl(data_.isCloneable(),							"			return (${className}) super.clone();");
		gl(data_.isCloneable(),							"		} catch (CloneNotSupportedException e) {");
		gl(data_.isCloneable(),							"			return null; // cannot happen");
		gl(data_.isCloneable(),							"		}");
		gl(data_.isCloneable(),							"	}");

		// If any bound properties exist, add PropertyChangeSupport and delegate methods for it
		gl(data_.isAtLeastOneBound(),					"	private java.beans.PropertyChangeSupport propertyChangeSupport_ = new java.beans.PropertyChangeSupport(this);");
		gl(data_.isAtLeastOneBound(),					"	protected java.beans.PropertyChangeSupport getPropertyChangeSupport() {");
		gl(data_.isAtLeastOneBound(),					"		return propertyChangeSupport_;");
		gl(data_.isAtLeastOneBound(),					"	}");
		gl(data_.isAtLeastOneBound(),					"	public void addPropertyChangeListener(java.beans.PropertyChangeListener listener) {");
		gl(data_.isAtLeastOneBound(),					"		getPropertyChangeSupport().addPropertyChangeListener(listener);");
		gl(data_.isAtLeastOneBound(),					"	}");
		gl(data_.isAtLeastOneBound(),					"	public void addPropertyChangeListener(java.lang.String propertyName, java.beans.PropertyChangeListener listener) {");
		gl(data_.isAtLeastOneBound(),					"		getPropertyChangeSupport().addPropertyChangeListener(propertyName, listener);");
		gl(data_.isAtLeastOneBound(),					"	}");
		gl(data_.isAtLeastOneBound(),					"	public void removePropertyChangeListener(java.beans.PropertyChangeListener listener) {");
		gl(data_.isAtLeastOneBound(),					"		getPropertyChangeSupport().removePropertyChangeListener(listener);");
		gl(data_.isAtLeastOneBound(),					"	}");
		gl(data_.isAtLeastOneBound(),					"	public void removePropertyChangeListener(java.lang.String propertyName, java.beans.PropertyChangeListener listener) {");
		gl(data_.isAtLeastOneBound(),					"		getPropertyChangeSupport().removePropertyChangeListener(propertyName, listener);");
		gl(data_.isAtLeastOneBound(),					"	}");

		final boolean define = data_.isDefinePropertyNameConstants();
		final boolean extend = data_.isExtendPropertyNameConstants();
		final String sup = data_.getSuperclass();
		gp(define,										"	public interface PropertyNames");
		gp(define && extend && sup != null,				" extends " + sup + ".PropertyNames");
		gl(define,										" {");
		new ForEach<PropertySpec>(data_.getProperties()) {
			@Override protected void go(PropertySpec property) {
				gl(define,								"		static final String ${name} = \"${name}\";");
			}};
		gl(define,										"	}");

		new ForEach<PropertySpec>(data_.getProperties()) {
			@Override protected void go(PropertySpec property) {
				boolean readable = property.isReadable();
				boolean writeable = property.isWriteable();
				boolean notNull = property.isNotNull();
				boolean bound = property.isBound();
				boolean simple = property.getKind().isSimple();
				boolean list = property.getKind().isList();
				boolean set = property.getKind().isSet();
				boolean map = property.getKind().isMap();

                if ("boolean".equals(property.getType())) {
                    symbols_.pushScope("isOrGet", "is");
                } else {
                	symbols_.pushScope("isOrGet", "get");
                }

                // field definitions
				gl(simple,								"	private ${extraFieldKeywords}${type} ${name}_;");
				gl(list,								"	private ${extraFieldKeywords}final java.util.List<${type}> ${pluralName}_ = new java.util.ArrayList<${type}>();");
				gl(set,									"	private ${extraFieldKeywords}final java.util.Set<${type}> ${pluralName}_ = new java.util.HashSet<${type}>();");
				gl(map,									"	private ${extraFieldKeywords}final java.util.Map<${keyType}, ${type}> ${pluralName}_ = new java.util.HashMap<${keyType}, ${type}>();");

				// getter and setter definitions for simple properties
				gl(simple && readable,					"	${readerAccess}${extraMethodKeywords}${type} ${isOrGet}${upper:name}() {");
				gl(simple && readable,					"		return ${name}_;");
				gl(simple && readable,					"	}");
				gl(simple && writeable,					"	${writerAccess}${extraMethodKeywords}void set${upper:name}(${type} value) {");
				gl(simple && writeable && notNull,		"		if (value == null) throw new IllegalArgumentException(\"${name} cannot be null\");");
				gl(simple && writeable && bound,		"		${type} oldValue = ${name}_;");
				gl(simple && writeable,					"		${name}_ = value;");
				gl(simple && writeable && bound,		"		getPropertyChangeSupport().firePropertyChange(\"${name}\", oldValue, value);");
				gl(simple && writeable,					"	}");

				// getter and setter definitions for list properties
				gl(list && readable,					"	${readerAccess}${extraMethodKeywords}${type} ${isOrGet}${upper:name}(int i) {");
				gl(list && readable,					"		return ${pluralName}_.get(i);");
				gl(list && readable,					"	}");
				gl(list && readable,					"	${readerAccess}${extraMethodKeywords}java.util.List<${type}> get${upper:pluralName}() {");
				gl(list && readable,					"		return ${unmodPrefix}${pluralName}_${unmodSuffix};");
				gl(list && readable,					"	}");
				gl(list && writeable,					"	${writerAccess}${extraMethodKeywords}${type} remove${upper:name}(int i) {");
				gl(list && writeable,					"		${type} result = ${pluralName}_.remove(i);");
				gl(list && writeable && bound,			"		getPropertyChangeSupport().firePropertyChange(\"${pluralName}\", null, ${pluralName}_);");
				gl(list && writeable,					"		return result;");
				gl(list && writeable,					"	}");
				gl(list && writeable,					"	${writerAccess}${extraMethodKeywords}void add${upper:name}(int i, ${type} value) {");
				gl(list && writeable,					"		if (value == null) throw new IllegalArgumentException(\"Cannot add null to ${name}\");");
				gl(list && writeable,					"		${pluralName}_.add(i, value);");
				gl(list && writeable && bound,			"		getPropertyChangeSupport().firePropertyChange(\"${pluralName}\", null, ${pluralName}_);");
				gl(list && writeable,					"	}");

				// getter and setter definitions for set properties
				gl(set && readable,						"	${readerAccess}${extraMethodKeywords}java.util.Set<${type}> get${upper:pluralName}() {");
				gl(set && readable,						"		return ${unmodPrefix}${pluralName}_${unmodSuffix};");
				gl(set && readable,						"	}");

				// getter and setter definitions for set OR list properties
				gl((list || set) && readable,			"	${readerAccess}${extraMethodKeywords}boolean ${pluralName}Contains(${type} value) {");
				gl((list || set) && readable,			"		return ${pluralName}_.contains(value);");
				gl((list || set) && readable,			"	}");
				gl((list || set) && writeable,			"	${writerAccess}${extraMethodKeywords}void add${upper:name}(${type} value) {");
				gl((list || set) && writeable,			"		if (value == null) throw new IllegalArgumentException(\"Cannot add null to ${name}\");");
				gl((list || set) && writeable,			"		${pluralName}_.add(value);");
				gl((list || set) && writeable && bound,	"		getPropertyChangeSupport().firePropertyChange(\"${pluralName}\", null, ${pluralName}_);");
				gl((list || set) && writeable,			"	}");
				gl((list || set) && writeable,			"	${writerAccess}${extraMethodKeywords}boolean remove${upper:name}(${type} value) {");
				gl((list || set) && writeable,			"		if (value == null) throw new IllegalArgumentException(\"Cannot remove null from ${name}\");");
				gl((list || set) && writeable,			"		boolean result = ${pluralName}_.remove(value);");
				gl((list || set) && writeable && bound,	"		getPropertyChangeSupport().firePropertyChange(\"${pluralName}\", null, ${pluralName}_);");
				gl((list || set) && writeable,			"		return result;");
				gl((list || set) && writeable,			"	}");
				gl((list || set) && writeable,			"	${writerAccess}${extraMethodKeywords}void clear${upper:pluralName}() {");
		    	gl((list || set) && writeable,			"		${pluralName}_.clear();");
				gl((list || set) && writeable && bound,	"		getPropertyChangeSupport().firePropertyChange(\"${pluralName}\", null, ${pluralName}_);");
				gl((list || set) && writeable,			"	}");

				// getter and setter definitions for map properties
				gl(map && readable,						"	${readerAccess}${extraMethodKeywords}${type} get${upper:name}(${keyType} key) {");
				gl(map && readable,						"		return ${pluralName}_.get(key);");
				gl(map && readable,						"	}");
				gl(map && readable,						"	${readerAccess}${extraMethodKeywords}java.util.Map<${keyType}, ${type}> get${upper:pluralName}() {");
				gl(map && readable,						"		return ${unmodPrefix}${pluralName}_${unmodSuffix};");
				gl(map && readable,						"	}");
				gl(map && readable,						"	${readerAccess}${extraMethodKeywords}boolean ${pluralName}ContainsKey(${keyType} key) {");
				gl(map && readable,						"		return ${pluralName}_.containsKey(key);");
				gl(map && readable,						"	}");
				gl(map && readable,						"	${readerAccess}${extraMethodKeywords}boolean ${pluralName}ContainsValue(${type} value) {");
				gl(map && readable,						"		return ${pluralName}_.containsValue(value);");
				gl(map && readable,						"	}");
				gl(map && writeable,					"	${writerAccess}${extraMethodKeywords}void put${upper:name}(${keyType} key, ${type} value) {");
				gl(map && writeable,					"		if (key == null) throw new IllegalArgumentException(\"Cannot put null key in ${name}\");");
				gl(map && writeable,					"		if (value == null) throw new IllegalArgumentException(\"Cannot put null value in ${name}\");");
				gl(map && writeable,					"		${pluralName}_.put(key, value);");
				gl(map && writeable && bound,			"		getPropertyChangeSupport().firePropertyChange(\"${pluralName}\", null, ${pluralName}_);");
				gl(map && writeable,					"	}");
				gl(map && writeable,					"	${writerAccess}${extraMethodKeywords}${type} remove${upper:name}(${keyType} key) {");
				gl(map && writeable,					"		if (key == null) throw new IllegalArgumentException(\"Cannot remove null key from ${name}\");");
				gl(map && writeable,					"		${type} result = ${pluralName}_.remove(key);");
				gl(map && writeable && bound,			"		getPropertyChangeSupport().firePropertyChange(\"${pluralName}\", null, ${pluralName}_);");
				gl(map && writeable,					"		return result;");
				gl(map && writeable,					"	}");
				gl(map && writeable,					"	${writerAccess}${extraMethodKeywords}void clear${upper:pluralName}() {");
				gl(map && writeable,					"		${pluralName}_.clear();");
				gl(map && writeable && bound,			"		getPropertyChangeSupport().firePropertyChange(\"${pluralName}\", null, ${pluralName}_);");
				gl(map && writeable,					"	}");

				symbols_.popScope(); // pop the isOrGet scope
			}}; // end foreach property spec

		// define default methods
		new ForEach<Method>(data_.getDefaultMethods()) {
			@Override protected void go(Method method) {
				boolean returns = !"void".equals(method.getReturnType());
				gl(method.isAbstract(),					"	${access}abstract ${returnType} ${name}(${argDecls})${throwsClause};");
				gl(!method.isAbstract(),				"	${access} ${returnType} ${name}(${argDecls})${throwsClause} {");
				gp(!method.isAbstract() && returns,		"		return ");
				gl(!method.isAbstract(),				"		${name}(${args});");
				gl(!method.isAbstract(),				"	}");
			}};


		// define delegate methods
		new ForEach<DelegateSpec>(data_.getDelegates()) {
			@Override protected void go(final DelegateSpec delegate) {
				new ForEach<Method>(delegate.getMethods()) {
					@Override protected void go(Method method) {
						boolean returns = !"void".equals(method.getReturnType());
						gl(								"	public ${returnType} ${name}(${argDecls})${throwsClause} {");
						gp(								"		");
						gp(returns,						"return ");
						gl(								"${accessor}.${name}(${args});"); // accessor is from DelegateSpec
						gl(								"	}");
					}};
			}};

		// define null object implementations
		new ForEach<Type>(data_.getNullImplementations()) {
			@Override protected void go(final Type nullObject) {
				new ForEach<Method>(nullObject.getMethods()) {
					@Override protected void go(Method method) {
						gl(								"	public ${returnType} ${name}(${argDecls})${throwsClause} {");
						gl(								"		${nullBody}");
						gl(								"	}");
					}};
			}};

		// Define observer management and fire methods
		//	TODO - what if the methods are declared to throw exceptions (like PropertyVetoException)?
		new ForEach<Type>(data_.getObservers()) {
			@Override protected void go(final Type observer) {
	    	    gl(										"	private java.util.List<${name}> ${stripPackage:lower:name}s_ = new java.util.ArrayList<${name}>();");
	    	    gl(										"	public void add${stripPackage:name}(${name} listener) {");
	    	    gl(										"		synchronized(${stripPackage:lower:name}s_) {");
	    	    gl(										"			${stripPackage:lower:name}s_.add(listener);");
	    	    gl(										"		}");
	    	    gl(										"	}");
	    	    gl(										"	public void remove${stripPackage:name}(${name} listener) {");
	    	    gl(										"		synchronized(${stripPackage:lower:name}s_) {");
	    	    gl(										"			${stripPackage:lower:name}s_.remove(listener);");
	    	    gl(										"		}");
	    	    gl(										"	}");
	    	    new ForEach<Method>(observer.getMethods()) {
	    	    	@Override protected void go(Method method) {
	    	    		gl(								"	protected void fire${upper:name}(${argDecls}) {");
	    	    		gl(								"		java.util.List<${parent:name}> targets = null;");
	    	    		gl(								"		synchronized(${parent:stripPackage:lower:name}s_) {");
	    	    		gl(								"			targets = new java.util.ArrayList<${parent:name}>(${parent:stripPackage:lower:name}s_);");
	    	    		gl(								"		}");
	    	    		gl(								"		for (${parent:name} listener : targets) {");
	    	    		gl(								"			listener.${name}(${args});");
	    	    		gl(								"		}");
	    	    		gl(								"	}");
	    	    	}};
			}};

		// Define simple equals() and hashCode() methods
		if (data_.isDefineSimpleEqualsAndHashCode()) {
			gl(											"	protected boolean checkEquals(java.lang.Object o1, java.lang.Object o2) {");
			gl(											"		if (o1 == o2) return true;");
			gl(											"		if (o1 == null || o2 == null) return false;");
			gl(											"		return o1.equals(o2);");
			gl(											"	}");

			gl(											"	public boolean equals(java.lang.Object obj) {");
			gl(											"		if (obj == this) return true;");

			gl(data_.isEqualsShouldCheckSuperEquals(),	"		if (obj == null || obj.getClass() != getClass() || !super.equals(obj)) return false;");
			gl(!data_.isEqualsShouldCheckSuperEquals(),	"		if (obj == null || obj.getClass() != getClass()) return false;");
			gl(data_.getProperties().isEmpty(),			"		return true;");
			gl(!data_.getProperties().isEmpty(),		"		${className}Gen other = (${className}Gen) obj;");
			gl(!data_.getProperties().isEmpty(),		"		return ");
			new ForEach<PropertySpec>(data_.getProperties()) {
				@Override protected void go(PropertySpec property) {
					boolean simple = property.getKind().isSimple();
					boolean primitive = property.isPrimitive();
					gp(primitive,						"		       other.${name}_ == ${name}_");
					gp(!primitive && !simple,			"		       checkEquals(other.${pluralName}_, ${pluralName}_)");
					gp(!primitive && simple,			"		       checkEquals(other.${name}_, ${name}_)");
					gl(isLast(),						";");
					gl(!isLast(),						" &&");
				}};
			gl(											"	}");

			// TODO: better hashcode algorithm for primitives
			gl(											"	public int hashCode() {");
			gl(data_.getProperties().isEmpty(),			"		return super.hashCode();");
			gl(!data_.getProperties().isEmpty(),		"		return super.hashCode() +");
			new ForEach<PropertySpec>(data_.getProperties()) {
				@Override protected void go(PropertySpec property) {
					boolean primitive = property.isPrimitive();
					boolean simple = property.getKind().isSimple();
	                gp(primitive,						"			${intConversion}");
	                gp(!primitive && simple,			"			${name}_.hashCode()");
	                gp(!primitive && !simple,			"			${pluralName}_.hashCode()");
					gl(!isLast(),						" +");
					gl(isLast(),						";");
				}};
			gl(											"	}");
		}

		//	Define a nice default toString method
		gl(												"	public java.lang.String toString() {");
		gl(												"		return getClass().getName() + '[' + paramString() + ']';");
		gl(												"	}");
		gl(												"	protected java.lang.String paramString() {");
		gl(data_.getProperties().isEmpty(),				"		return \"\";");
		gl(!data_.getProperties().isEmpty(),			"		return");
		new ForEach<PropertySpec>(data_.getProperties()) {
			@Override protected void go(PropertySpec property) {
				boolean simple = property.getKind().isSimple();
				boolean omitValue = property.isOmitFromToString();
				gp(!omitValue,							"			\"");
				gp(!isFirst() && !omitValue,			",");
				gp(simple && !omitValue,				"${name}=\" + ${name}_");
				gp(!simple && !omitValue,				"${pluralName}=\" + ${pluralName}_");
				gp(simple && omitValue,					"${name}=\" + '<' + ${name}_.getClass().getName() + '>'");
				gp(!simple && omitValue,				"${pluralName}=\" + '<' + ${pluralName}_.getClass().getName() + '>'");
				gl(!isLast() && !omitValue,				" +");
				gl(isLast()&& !omitValue,				";");
			}};
		gl(												"	}");

		if (data_.isCreatePropertyMap()) {
			gl(											"	public java.util.Map<java.lang.String, java.lang.Object> createPropertyMap() {");
			gl(!data_.isCreatePropertyMapCallsSuper(),	"		java.util.Map<java.lang.String, java.lang.Object> map = new java.util.HashMap<java.lang.String, java.lang.Object>();");
			gl(data_.isCreatePropertyMapCallsSuper(),	"		java.util.Map<java.lang.String, java.lang.Object> map = super.createPropertyMap();");
			new ForEach<PropertySpec>(data_.getProperties()) {
				@Override protected void go(PropertySpec property) {
					if (property.isReadable()) {
						boolean simple = property.getKind().isSimple();
		                if ("boolean".equals(property.getType())) {
		                    symbols_.pushScope("isOrGet", "is");
		                } else {
		                	symbols_.pushScope("isOrGet", "get");
		                }

						gl(simple,						"		map.put(\"${name}\", ${isOrGet}${upper:name}());");
						gl(!simple,						"		map.put(\"${pluralName}\", ${isOrGet}${upper:pluralName}());");
					}
					symbols_.popScope();
				}};
			gl(											"		return map;");
			gl(											"	}");
		}

		gl("}"); // close off the class
	}
}
